--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
meshchatx/src/backend/http/middleware.py 22b4fcce096023ec706bab5449805e34a2912643 (22b4fcce) Text, 15.78 KB
T8b949e# SPDX-License-Identifier: 0BSD
T8b949e"""aiohttp middleware factories for MeshChatX HTTP.
Factories take the live app instance and return middleware callables.
Order returned by register_all_routes / _define_routes must remain:
auth, mime_type, security, csrf, ip_allowlist.
"""
Tff7b72from T7ee787__future__ Tff7b72import Te6edf3annotations
Tff7b72from T7ee787urllibT7ee787.T7ee787parse Tff7b72import Te6edf3urlparse
Tff7b72from T7ee787aiohttp Tff7b72import Te6edf3web
Tff7b72from T7ee787aiohttp_session Tff7b72import Te6edf3get_session
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backendT7ee787.T7ee787app_security_settings Tff7b72import Tb4b4b4(
Te6edf3get_trusted_proxy_cidrsTb4b4b4,
Te6edf3get_web_ui_ip_allowlistTb4b4b4,
Tb4b4b4)
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backendT7ee787.T7ee787csrf Tff7b72import Te6edf3validate_csrf_header
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backendT7ee787.T7ee787ip_allowlist Tff7b72import Te6edf3client_ip_allowed
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backendT7ee787.T7ee787privacy_mode Tff7b72import Te6edf3privacy_mode_enabled
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787env_utils Tff7b72import Te6edf3env_bool
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787path_utils Tff7b72import Te6edf3request_client_ip
Tff7b72def Td2a8ffcsrf_exempt_pathTb4b4b4(Te6edf3pathTb4b4b4: Tffa657strTb4b4b4) Tff7b72-Tff7b72> Tffa657boolTb4b4b4:
Tff7b72return Te6edf3path Tff7b72== Ta5d6ff"Ta5d6ff/api/v1/auth/csrfTa5d6ff"
Tff7b72def Td2a8ffcreate_ip_allowlist_middlewareTb4b4b4(Te6edf3appTb4b4b4)Tb4b4b4:
Tf0883e@webTff7b72.Td2a8ffmiddleware
Tff7b72async Tff7b72def Td2a8ffip_allowlist_middlewareTb4b4b4(Te6edf3requestTb4b4b4, Te6edf3handlerTb4b4b4)Tb4b4b4:
Te6edf3path Tff7b72= Te6edf3requestTff7b72.Td2a8ffpath
Tff7b72if Te6edf3path Tff7b72== Ta5d6ff"Ta5d6ff/api/v1/statusTa5d6ff"Tb4b4b4:
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
Te6edf3allowlist Tff7b72= Te6edf3get_web_ui_ip_allowlistTb4b4b4(Te6edf3appTff7b72.Td2a8ffstorage_dirTb4b4b4)
Tff7b72if Te6edf3allowlistTb4b4b4:
Te6edf3ip Tff7b72= Te6edf3request_client_ipTb4b4b4(Te6edf3requestTb4b4b4, Te6edf3get_trusted_proxy_cidrsTb4b4b4(Te6edf3appTff7b72.Td2a8ffstorage_dirTb4b4b4)Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3client_ip_allowedTb4b4b4(Te6edf3ipTb4b4b4, Te6edf3allowlistTb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ff/api/Ta5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3webTff7b72.Td2a8ffjson_responseTb4b4b4(
Tb4b4b4{Ta5d6ff"Ta5d6fferrorTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffForbidden: client IP not on allowlistTa5d6ff"Tb4b4b4}Tb4b4b4,
Te6edf3statusTff7b72=T79c0ff403Tb4b4b4,
Tb4b4b4)
Tff7b72return Te6edf3webTff7b72.Td2a8ffResponseTb4b4b4(
Te6edf3textTff7b72=Ta5d6ff"Ta5d6ffForbiddenTa5d6ff"Tb4b4b4,
Te6edf3statusTff7b72=T79c0ff403Tb4b4b4,
Te6edf3headersTff7b72=Tb4b4b4{Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6fftext/htmlTa5d6ff"Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
Tff7b72return Te6edf3ip_allowlist_middleware
Tff7b72def Td2a8ffcreate_csrf_middlewareTb4b4b4(Te6edf3appTb4b4b4)Tb4b4b4:
Tf0883e@webTff7b72.Td2a8ffmiddleware
Tff7b72async Tff7b72def Td2a8ffcsrf_middlewareTb4b4b4(Te6edf3requestTb4b4b4, Te6edf3handlerTb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3env_boolTb4b4b4(Ta5d6ff"Ta5d6ffMESHCHAT_DISABLE_CSRFTa5d6ff"Tb4b4b4, Tff7b72FalseTb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
Tff7b72if Te6edf3requestTff7b72.Td2a8ffmethod Tff7b72in Tb4b4b4(Ta5d6ff"Ta5d6ffGETTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffHEADTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffOPTIONSTa5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
Te6edf3path Tff7b72= Te6edf3requestTff7b72.Td2a8ffpath
Tff7b72if Tff7b72not Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ff/api/Ta5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
Tff7b72if Te6edf3csrf_exempt_pathTb4b4b4(Te6edf3pathTb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
Tff7b72tryTb4b4b4:
Te6edf3session Tff7b72= Tff7b72await Te6edf3get_sessionTb4b4b4(Te6edf3requestTb4b4b4)
Tff7b72except Tf85149ExceptionTb4b4b4:
Tff7b72return Te6edf3webTff7b72.Td2a8ffjson_responseTb4b4b4(
Tb4b4b4{Ta5d6ff"Ta5d6fferrorTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffSession required for CSRF validationTa5d6ff"Tb4b4b4}Tb4b4b4,
Te6edf3statusTff7b72=T79c0ff403Tb4b4b4,
Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3validate_csrf_headerTb4b4b4(Te6edf3requestTb4b4b4, Te6edf3sessionTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3webTff7b72.Td2a8ffjson_responseTb4b4b4(
Tb4b4b4{Ta5d6ff"Ta5d6fferrorTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffInvalid or missing CSRF tokenTa5d6ff"Tb4b4b4}Tb4b4b4,
Te6edf3statusTff7b72=T79c0ff403Tb4b4b4,
Tb4b4b4)
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
Tff7b72return Te6edf3csrf_middleware
Tff7b72def Td2a8ffcreate_auth_middlewareTb4b4b4(Te6edf3appTb4b4b4)Tb4b4b4:
Tf0883e@webTff7b72.Td2a8ffmiddleware
Tff7b72async Tff7b72def Td2a8ffauth_middlewareTb4b4b4(Te6edf3requestTb4b4b4, Te6edf3handlerTb4b4b4)Tb4b4b4:
Te6edf3path Tff7b72= Te6edf3requestTff7b72.Td2a8ffpath
T8b949e# Health check for startup probes (Electron loading page, monitors).
Tff7b72if Te6edf3path Tff7b72== Ta5d6ff"Ta5d6ff/api/v1/statusTa5d6ff"Tb4b4b4:
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
T8b949e# Allow CSRF bootstrap and auth status while the network stack starts so the
T8b949e# Vue shell can load and show an in-app waiting state.
Tff7b72if Te6edf3path Tff7b72in Tb4b4b4(
Ta5d6ff"Ta5d6ff/api/v1/auth/csrfTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/api/v1/auth/statusTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/api/v1/auth/altcha/challengeTa5d6ff"Tb4b4b4,
Tb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
T8b949e# Serve the web UI shell and static files while an identity context is still
T8b949e# starting, so the browser can load assets and show in-app loading state.
Tff7b72if Tff7b72not Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ff/api/Ta5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72if Tb4b4b4(
Te6edf3path Tff7b72== Ta5d6ff"Ta5d6ff/Ta5d6ff"
Tff7b72or Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(
Tb4b4b4(
Ta5d6ff"Ta5d6ff/assets/Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/favicons/Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/reticulum-docs/Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/meshchatx-docs/Ta5d6ff"Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tff7b72or Te6edf3path Tff7b72in Tb4b4b4(Ta5d6ff"Ta5d6ff/manifest.jsonTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff/service-worker.jsTa5d6ff"Tb4b4b4)
Tff7b72or Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(
Tb4b4b4(
Ta5d6ff"Ta5d6ff.jsTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.cssTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.jsonTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.wasmTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.pngTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.jpgTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.jpegTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.icoTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.svgTa5d6ff"Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
Tff7b72if Tff7b72not Te6edf3appTff7b72.Td2a8ffcurrent_context Tff7b72or Tff7b72not Te6edf3appTff7b72.Td2a8ffcurrent_contextTff7b72.Td2a8ffrunningTb4b4b4:
Tff7b72return Te6edf3webTff7b72.Td2a8ffjson_responseTb4b4b4(
Tb4b4b4{
Ta5d6ff"Ta5d6fferrorTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffApplication is initializing or switching identityTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffstatusTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffstartingTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffstageTa5d6ff"Tb4b4b4: Te6edf3appTff7b72.Td2a8ff_startup_stageTb4b4b4,
Ta5d6ff"Ta5d6ffnetwork_readyTa5d6ff"Tb4b4b4: Tff7b72FalseTb4b4b4,
Tb4b4b4}Tb4b4b4,
Te6edf3statusTff7b72=T79c0ff503Tb4b4b4,
Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3appTff7b72.Td2a8ffauth_enabledTb4b4b4:
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
T8b949e# allow access to auth endpoints and setup page
Te6edf3public_paths Tff7b72= Tb4b4b4[
Ta5d6ff"Ta5d6ff/api/v1/statusTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/api/v1/auth/csrfTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/api/v1/auth/setupTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/api/v1/auth/loginTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/api/v1/auth/statusTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/api/v1/auth/logoutTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/api/v1/auth/altcha/challengeTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/manifest.jsonTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/service-worker.jsTa5d6ff"Tb4b4b4,
Tb4b4b4]
T8b949e# check if path is public
Te6edf3is_public Tff7b72= Tffa657anyTb4b4b4(Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(Te6edf3publicTb4b4b4) Tff7b72for Te6edf3public Tff7b72in Te6edf3public_pathsTb4b4b4)
Tff7b72if Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(Tb4b4b4(Ta5d6ff"Ta5d6ff/reticulum-docs/Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff/meshchatx-docs/Ta5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4:
Te6edf3is_public Tff7b72= Tff7b72True
T8b949e# check if requesting setup page (index.html will show setup if needed)
Tff7b72if Tb4b4b4(
Te6edf3path Tff7b72== Ta5d6ff"Ta5d6ff/Ta5d6ff"
Tff7b72or Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(
Tb4b4b4(Ta5d6ff"Ta5d6ff/assets/Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff/favicons/Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff/reticulum-docs/Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff/meshchatx-docs/Ta5d6ff"Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tff7b72or Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(
Tb4b4b4(
Ta5d6ff"Ta5d6ff.jsTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.cssTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.jsonTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.wasmTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.pngTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.jpgTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.jpegTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.icoTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff.svgTa5d6ff"Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4)Tb4b4b4:
Te6edf3is_public Tff7b72= Tff7b72True
Tff7b72if Te6edf3is_publicTb4b4b4:
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
T8b949e# check authentication
Tff7b72tryTb4b4b4:
Te6edf3session Tff7b72= Tff7b72await Te6edf3get_sessionTb4b4b4(Te6edf3requestTb4b4b4)
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffSession decryption failed: Tffd700{Te6edf3eTffd700}Ta5d6ff"Tb4b4b4)
T8b949e# If decryption fails, we must treat as unauthenticated
Tff7b72if Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ff/api/Ta5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3webTff7b72.Td2a8ffjson_responseTb4b4b4(
Tb4b4b4{Ta5d6ff"Ta5d6fferrorTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffSession expired or invalid. Please login again.Ta5d6ff"Tb4b4b4}Tb4b4b4,
Te6edf3statusTff7b72=T79c0ff401Tb4b4b4,
Tb4b4b4)
Tff7b72return Te6edf3webTff7b72.Td2a8ffResponseTb4b4b4(
Te6edf3textTff7b72=Ta5d6ff"Ta5d6ffAuthentication requiredTa5d6ff"Tb4b4b4,
Te6edf3statusTff7b72=T79c0ff401Tb4b4b4,
Te6edf3headersTff7b72=Tb4b4b4{Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6fftext/htmlTa5d6ff"Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Te6edf3is_authenticated Tff7b72= Te6edf3sessionTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffauthenticatedTa5d6ff"Tb4b4b4, Tff7b72FalseTb4b4b4)
Te6edf3session_identity Tff7b72= Te6edf3sessionTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffidentity_hashTa5d6ff"Tb4b4b4)
T8b949e# Check if authenticated AND matches current identity
Tff7b72if Tff7b72not Te6edf3is_authenticated Tff7b72or Te6edf3session_identity Tff7b72!= Te6edf3appTff7b72.Td2a8ffidentityTff7b72.Td2a8ffhashTff7b72.Td2a8ffhexTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ff/api/Ta5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3webTff7b72.Td2a8ffjson_responseTb4b4b4(
Tb4b4b4{Ta5d6ff"Ta5d6fferrorTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffAuthentication requiredTa5d6ff"Tb4b4b4}Tb4b4b4,
Te6edf3statusTff7b72=T79c0ff401Tb4b4b4,
Tb4b4b4)
Tff7b72return Te6edf3webTff7b72.Td2a8ffResponseTb4b4b4(
Te6edf3textTff7b72=Ta5d6ff"Ta5d6ffAuthentication requiredTa5d6ff"Tb4b4b4,
Te6edf3statusTff7b72=T79c0ff401Tb4b4b4,
Te6edf3headersTff7b72=Tb4b4b4{Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6fftext/htmlTa5d6ff"Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tff7b72return Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
Tff7b72return Te6edf3auth_middleware
Tff7b72def Td2a8ffcreate_mime_type_middlewareTb4b4b4(Te6edf3appTb4b4b4)Tb4b4b4:
Tf0883e@webTff7b72.Td2a8ffmiddleware
Tff7b72async Tff7b72def Td2a8ffmime_type_middlewareTb4b4b4(Te6edf3requestTb4b4b4, Te6edf3handlerTb4b4b4)Tb4b4b4:
Te6edf3response Tff7b72= Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
Tff7b72if Te6edf3response Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72return Tff7b72None
Te6edf3path Tff7b72= Te6edf3requestTff7b72.Td2a8ffpath
Tff7b72if Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ff/api/Ta5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3response
Tff7b72if Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(Tb4b4b4(Ta5d6ff"Ta5d6ff.jsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff.mjsTa5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffapplication/javascript; charset=utf-8Ta5d6ff"
Tff7b72elif Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(Ta5d6ff"Ta5d6ff.cssTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6fftext/css; charset=utf-8Ta5d6ff"
Tff7b72elif Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(Ta5d6ff"Ta5d6ff.jsonTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffapplication/json; charset=utf-8Ta5d6ff"
Tff7b72elif Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(Ta5d6ff"Ta5d6ff.wasmTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffapplication/wasmTa5d6ff"
Tff7b72elif Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(Ta5d6ff"Ta5d6ff.htmlTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6fftext/html; charset=utf-8Ta5d6ff"
Tff7b72elif Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(Ta5d6ff"Ta5d6ff.mdTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6fftext/markdown; charset=utf-8Ta5d6ff"
Tff7b72elif Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(Ta5d6ff"Ta5d6ff.txtTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6fftext/plain; charset=utf-8Ta5d6ff"
Tff7b72elif Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(Ta5d6ff"Ta5d6ff.opusTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffaudio/opusTa5d6ff"
Tff7b72elif Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(Ta5d6ff"Ta5d6ff.oggTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffaudio/oggTa5d6ff"
Tff7b72elif Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(Ta5d6ff"Ta5d6ff.wavTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffaudio/wavTa5d6ff"
Tff7b72elif Te6edf3pathTff7b72.Td2a8ffendswithTb4b4b4(Ta5d6ff"Ta5d6ff.mp3Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffContent-TypeTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffaudio/mpegTa5d6ff"
Tff7b72return Te6edf3response
Tff7b72return Te6edf3mime_type_middleware
Tff7b72def Td2a8ffcreate_security_middlewareTb4b4b4(Te6edf3appTb4b4b4)Tb4b4b4:
Tf0883e@webTff7b72.Td2a8ffmiddleware
Tff7b72async Tff7b72def Td2a8ffsecurity_middlewareTb4b4b4(Te6edf3requestTb4b4b4, Te6edf3handlerTb4b4b4)Tb4b4b4:
Te6edf3response Tff7b72= Tff7b72await Te6edf3handlerTb4b4b4(Te6edf3requestTb4b4b4)
Tff7b72if Te6edf3response Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72return Tff7b72None
T8b949e# Add security headers to all responses
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffX-Content-Type-OptionsTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffnosniffTa5d6ff"
T8b949e# Allow framing for docs and rnode flasher
Tff7b72if Te6edf3requestTff7b72.Td2a8ffpathTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ff/reticulum-docs/Ta5d6ff"Tb4b4b4) Tff7b72or Te6edf3requestTff7b72.Td2a8ffpathTff7b72.Td2a8ffstartswithTb4b4b4(
Ta5d6ff"Ta5d6ff/rnode-flasher/Ta5d6ff"Tb4b4b4,
Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffX-Frame-OptionsTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffSAMEORIGINTa5d6ff"
Tff7b72elseTb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffX-Frame-OptionsTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffDENYTa5d6ff"
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffX-XSS-ProtectionTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ff1; mode=blockTa5d6ff"
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffReferrer-PolicyTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffstrict-origin-when-cross-originTa5d6ff"
T8b949e# Explicitly allow mic/camera for this origin. Reverse proxies that omit
T8b949e# Permissions-Policy are fine. This documents intent for Brave and Chromium.
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffPermissions-PolicyTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffmicrophone=(self), camera=(self)Ta5d6ff"
T8b949e# CSP base configuration
Te6edf3privacy_mode Tff7b72= Te6edf3privacy_mode_enabledTb4b4b4(Te6edf3appTff7b72.Td2a8ffconfigTb4b4b4)
Te6edf3connect_sources Tff7b72= Tb4b4b4[
Ta5d6ff"Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffws://localhost:*Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffwss://localhost:*Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffblob:Ta5d6ff"Tb4b4b4,
Tb4b4b4]
Te6edf3img_sources Tff7b72= Tb4b4b4[
Ta5d6ff"Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffdata:Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffblob:Ta5d6ff"Tb4b4b4,
Tb4b4b4]
Tff7b72if Tff7b72not Te6edf3privacy_modeTb4b4b4:
Te6edf3connect_sourcesTff7b72.Td2a8ffextendTb4b4b4(
Tb4b4b4[
Ta5d6ff"Ta5d6ffhttps://*.tile.openstreetmap.orgTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffhttps://tile.openstreetmap.orgTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffhttps://nominatim.openstreetmap.orgTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffhttps://*.cartocdn.comTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffhttps://tiles.openfreemap.orgTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffhttps://*.openfreemap.orgTa5d6ff"Tb4b4b4,
Tb4b4b4]Tb4b4b4,
Tb4b4b4)
Te6edf3img_sourcesTff7b72.Td2a8ffextendTb4b4b4(
Tb4b4b4[
Ta5d6ff"Ta5d6ffhttps://*.tile.openstreetmap.orgTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffhttps://tile.openstreetmap.orgTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffhttps://*.cartocdn.comTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffhttps://tiles.openfreemap.orgTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffhttps://*.openfreemap.orgTa5d6ff"Tb4b4b4,
Tb4b4b4]Tb4b4b4,
Tb4b4b4)
Te6edf3frame_sources Tff7b72= Tb4b4b4[
Ta5d6ff"Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff"Tb4b4b4,
Tb4b4b4]
Te6edf3path Tff7b72= Te6edf3requestTff7b72.Td2a8ffpath
Tff7b72if Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ff/rnode-flasher/Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffAccess-Control-Allow-OriginTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ff*Ta5d6ff"
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffCross-Origin-Resource-PolicyTa5d6ff"Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffcross-originTa5d6ff"
Tff7b72if Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ff/rnode-flasher/Ta5d6ff"Tb4b4b4)Tb4b4b4:
T8b949e# Standalone RNode Flasher uses Vue in-DOM templates. The compileToFunction
T8b949e# relies on new Function(), which requires unsafe-eval.
Te6edf3script_sources Tff7b72= Tb4b4b4[
Ta5d6ff"Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff'Ta5d6ffunsafe-inlineTa5d6ff'Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff'Ta5d6ffunsafe-evalTa5d6ff'Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff'Ta5d6ffwasm-unsafe-evalTa5d6ff'Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffblob:Ta5d6ff"Tb4b4b4,
Tb4b4b4]
Tff7b72elif Te6edf3pathTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ff/reticulum-docs/Ta5d6ff"Tb4b4b4)Tb4b4b4:
T8b949e# blob: AudioWorklet addModule(blob:...) and similar dynamic scripts
Te6edf3script_sources Tff7b72= Tb4b4b4[
Ta5d6ff"Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff'Ta5d6ffunsafe-inlineTa5d6ff'Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff'Ta5d6ffwasm-unsafe-evalTa5d6ff'Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffblob:Ta5d6ff"Tb4b4b4,
Tb4b4b4]
Tff7b72elseTb4b4b4:
T8b949e# wasm-unsafe-eval: Codec2 / sox Emscripten WASM, and blob: worklets from object URLs
Te6edf3script_sources Tff7b72= Tb4b4b4[Ta5d6ff"Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff'Ta5d6ffwasm-unsafe-evalTa5d6ff'Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffblob:Ta5d6ff"Tb4b4b4]
Te6edf3style_sources Tff7b72= Tb4b4b4[Ta5d6ff"Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff'Ta5d6ffunsafe-inlineTa5d6ff'Ta5d6ff"Tb4b4b4]
Tff7b72if Te6edf3appTff7b72.Td2a8ffcurrent_context Tff7b72and Te6edf3appTff7b72.Td2a8ffcurrent_contextTff7b72.Td2a8ffconfig Tff7b72and Tff7b72not Te6edf3privacy_modeTb4b4b4:
T8b949e# Helper to add domain from URL
Tff7b72def Td2a8ffadd_domain_from_urlTb4b4b4(Te6edf3urlTb4b4b4, Te6edf3target_listTb4b4b4)Tb4b4b4:
Tff7b72if Tff7b72not Te6edf3urlTb4b4b4:
Tff7b72return Tff7b72None
Tff7b72tryTb4b4b4:
Te6edf3parsed Tff7b72= Te6edf3urlparseTb4b4b4(Te6edf3urlTb4b4b4)
Tff7b72if Te6edf3parsedTff7b72.Td2a8ffnetlocTb4b4b4:
Te6edf3domain Tff7b72= Ta5d6fffTa5d6ff"Tffd700{Te6edf3parsedTff7b72.Td2a8ffschemeTffd700}Ta5d6ff://Tffd700{Te6edf3parsedTff7b72.Td2a8ffnetlocTffd700}Ta5d6ff"
Tff7b72if Te6edf3domain Tff7b72not Tff7b72in Te6edf3target_listTb4b4b4:
Te6edf3target_listTff7b72.Td2a8ffappendTb4b4b4(Te6edf3domainTb4b4b4)
Tff7b72return Te6edf3domain
Tff7b72except Tf85149ExceptionTb4b4b4:
Tff7b72pass
Tff7b72return Tff7b72None
T8b949e# Add configured Gitea base URL
Te6edf3add_domain_from_urlTb4b4b4(
Te6edf3appTff7b72.Td2a8ffcurrent_contextTff7b72.Td2a8ffconfigTff7b72.Td2a8ffgitea_base_urlTff7b72.Td2a8ffgetTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3connect_sourcesTb4b4b4,
Tb4b4b4)
T8b949e# Add map tile server domain
Te6edf3map_tile_url Tff7b72= Te6edf3appTff7b72.Td2a8ffcurrent_contextTff7b72.Td2a8ffconfigTff7b72.Td2a8ffmap_tile_server_urlTff7b72.Td2a8ffgetTb4b4b4(Tb4b4b4)
Te6edf3add_domain_from_urlTb4b4b4(Te6edf3map_tile_urlTb4b4b4, Te6edf3img_sourcesTb4b4b4)
Te6edf3add_domain_from_urlTb4b4b4(Te6edf3map_tile_urlTb4b4b4, Te6edf3connect_sourcesTb4b4b4)
T8b949e# Add nominatim API domain
Te6edf3nominatim_url Tff7b72= Te6edf3appTff7b72.Td2a8ffcurrent_contextTff7b72.Td2a8ffconfigTff7b72.Td2a8ffmap_nominatim_api_urlTff7b72.Td2a8ffgetTb4b4b4(Tb4b4b4)
Te6edf3add_domain_from_urlTb4b4b4(Te6edf3nominatim_urlTb4b4b4, Te6edf3connect_sourcesTb4b4b4)
T8b949e# Add custom CSP sources from config
Tff7b72def Td2a8ffadd_extra_sourcesTb4b4b4(Te6edf3extra_strTb4b4b4, Te6edf3target_listTb4b4b4)Tb4b4b4:
Tff7b72if Tff7b72not Te6edf3extra_strTb4b4b4:
Tff7b72return
Te6edf3sources Tff7b72= Tb4b4b4[
Te6edf3sTff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tff7b72for Te6edf3s Tff7b72in Te6edf3extra_strTff7b72.Td2a8ffreplaceTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff,Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffreplaceTb4b4b4(Ta5d6ff"Ta5d6ff;Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff,Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffsplitTb4b4b4(Ta5d6ff"Ta5d6ff,Ta5d6ff"Tb4b4b4)
Tff7b72if Te6edf3sTff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tb4b4b4]
Tff7b72for Te6edf3s Tff7b72in Te6edf3sourcesTb4b4b4:
Tff7b72if Te6edf3s Tff7b72not Tff7b72in Te6edf3target_listTb4b4b4:
Te6edf3target_listTff7b72.Td2a8ffappendTb4b4b4(Te6edf3sTb4b4b4)
Te6edf3add_extra_sourcesTb4b4b4(
Te6edf3appTff7b72.Td2a8ffcurrent_contextTff7b72.Td2a8ffconfigTff7b72.Td2a8ffcsp_extra_connect_srcTff7b72.Td2a8ffgetTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3connect_sourcesTb4b4b4,
Tb4b4b4)
Te6edf3add_extra_sourcesTb4b4b4(
Te6edf3appTff7b72.Td2a8ffcurrent_contextTff7b72.Td2a8ffconfigTff7b72.Td2a8ffcsp_extra_img_srcTff7b72.Td2a8ffgetTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3img_sourcesTb4b4b4,
Tb4b4b4)
Te6edf3add_extra_sourcesTb4b4b4(
Te6edf3appTff7b72.Td2a8ffcurrent_contextTff7b72.Td2a8ffconfigTff7b72.Td2a8ffcsp_extra_frame_srcTff7b72.Td2a8ffgetTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3frame_sourcesTb4b4b4,
Tb4b4b4)
Te6edf3add_extra_sourcesTb4b4b4(
Te6edf3appTff7b72.Td2a8ffcurrent_contextTff7b72.Td2a8ffconfigTff7b72.Td2a8ffcsp_extra_script_srcTff7b72.Td2a8ffgetTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3script_sourcesTb4b4b4,
Tb4b4b4)
Te6edf3add_extra_sourcesTb4b4b4(
Te6edf3appTff7b72.Td2a8ffcurrent_contextTff7b72.Td2a8ffconfigTff7b72.Td2a8ffcsp_extra_style_srcTff7b72.Td2a8ffgetTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3style_sourcesTb4b4b4,
Tb4b4b4)
Te6edf3csp Tff7b72= Tb4b4b4(
Ta5d6ff"Ta5d6ffdefault-src Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff; Ta5d6ff"
Ta5d6fffTa5d6ff"Ta5d6ffscript-src Tffd700{Ta5d6ff'Ta5d6ff Ta5d6ff'Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3script_sourcesTb4b4b4)Tffd700}Ta5d6ff; Ta5d6ff"
Ta5d6fffTa5d6ff"Ta5d6ffstyle-src Tffd700{Ta5d6ff'Ta5d6ff Ta5d6ff'Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3style_sourcesTb4b4b4)Tffd700}Ta5d6ff; Ta5d6ff"
Ta5d6fffTa5d6ff"Ta5d6ffimg-src Tffd700{Ta5d6ff'Ta5d6ff Ta5d6ff'Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3img_sourcesTb4b4b4)Tffd700}Ta5d6ff; Ta5d6ff"
Tff7b72+ Tb4b4b4(
Ta5d6ff"Ta5d6fffont-src Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff data:; Ta5d6ff"
Tff7b72if Te6edf3privacy_mode
Tff7b72else Ta5d6ff"Ta5d6fffont-src Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff data: https://tiles.openfreemap.org https://*.openfreemap.org; Ta5d6ff"
Tb4b4b4)
Tff7b72+ Ta5d6fffTa5d6ff"Ta5d6ffconnect-src Tffd700{Ta5d6ff'Ta5d6ff Ta5d6ff'Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3connect_sourcesTb4b4b4)Tffd700}Ta5d6ff; Ta5d6ff"
Ta5d6ff"Ta5d6ffmedia-src Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff blob:; Ta5d6ff"
Ta5d6ff"Ta5d6ffworker-src Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff blob:; Ta5d6ff"
Ta5d6fffTa5d6ff"Ta5d6ffframe-src Tffd700{Ta5d6ff'Ta5d6ff Ta5d6ff'Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3frame_sourcesTb4b4b4)Tffd700}Ta5d6ff; Ta5d6ff"
Ta5d6ff"Ta5d6ffobject-src Ta5d6ff'Ta5d6ffnoneTa5d6ff'Ta5d6ff; Ta5d6ff"
Ta5d6ff"Ta5d6ffbase-uri Ta5d6ff'Ta5d6ffselfTa5d6ff'Ta5d6ff;Ta5d6ff"
Tb4b4b4)
Te6edf3responseTff7b72.Td2a8ffheadersTb4b4b4[Ta5d6ff"Ta5d6ffContent-Security-PolicyTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3csp
Tff7b72return Te6edf3response
Tff7b72return Te6edf3security_middleware
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────